home *** CD-ROM | disk | FTP | other *** search
Text File | 1997-08-10 | 21.3 KB | 494 lines | [TEXT/CWIE] |
- /*
- VBLInstall.c
-
- ********
- NEW FEATURE (9/5/96): You can now find out the time elapsed since the beginning of
- the last blanking. (Based on a suggestion of Stefan Treue.)
- vblData->microTicks is set to Microseconds at that time.
- You compute time elapsed by comparing that with the current time
- returned by Microseconds. All the Time Manager calls in VBLInstall.c have
- been replaced by Microseconds calls, which have less overhead (and equal accuracy).
- To read about how to use the Microseconds trap, i suggest Minow's article in Develop,
- "Timing on the Macintosh"
- <http://devworld.apple.com/dev/techsupport/develop/issue26/minow.html>
-
- 3/20/97:
- If you're running on PowerPC then the time, as returned by Seconds.c,
- is also stored in vblData->seconds, as a double. However, this feature
- is disabled when running on a 68K machine because doing floating point
- in an interrupt service routine is complicated when using the 68881 fpu,
- as explained in the Apple tech note:
- <http://devworld.apple.com/dev/technotes/hw/hw_22.html>
- And there's hardly any point since Seconds.c uses Microseconds() when
- running on a 68k machine, so the microTicks field has the same information,
- though admittedly in a slightly less convenient form. (You may wish to
- copy the code from Seconds.c to convert microTicks to seconds.)
- At present we disable the call to Seconds() if GENERATING68K. Strictly
- speaking, it would be enough to disable only if GENERATING68881, but it
- seems like a good idea to avoid spending a lot of time in the interrupt
- service routine (they block other interrupts), and floating operations are
- slow without an fpu.
- ***************
-
- VBLInstall.c implements the Apple-recommended way of synchronizing your program to a video
- display. In the Macintosh, interrupt service routines run at a high processor
- priority, i.e. they block interrupts while they run, so Apple advises that they
- be very quick, to avoid missing interrupts. That is the approach taken here,
- using the interrupt service routines solely to bump a frame counter and set a
- newFrame flag to true. Typically your main program will iteratively test the
- newFrame flag and, when it's true, clear it and do some action that you want to
- do once per frame, in synch with the video frames.
-
- OSErr VBLInstall(VBLTaskAndA5 *vblData,GDHandle device,int frames);
-
- The first argument is a pointer to a user-supplied data structure. "device" is a
- handle to the video screen that you want to synchronize to, or NULL if you want
- to synchronize to the System VBL rate (usually 60.15 Hz). The last argument,
- "frames", specifies how many times you want the interrupt to occur before the
- routine disables itself. If frames<0 then the interrupt will recur indefinitely.
-
- VBLInstall zeroes vblData->frame, and sets vblData->framesDesired and
- vblData->framesLeft equal to "frames". While vblData->framesLeft!=0, at each
- end-of-frame interrupt vblData->frame will be incremented and vblData->newFrame
- will be set to 1. vblData->framesLeft will be decremented if
- it's >0, but left alone if it's negative. If vblData->framesLeft is decremented
- to zero then the interrupt service routine is done, and won't reenable the
- interrupt.
-
- Here's a minimal program, extracted from NoiseVBL.c, that uses these routines to
- show a 100-frame movie, with a new image on each video frame:
-
- VBLTaskAndA5 noiseVBL;
- GDHandle device=GetMainDevice();
- int frames=100,error;
-
- noiseVBL.subroutine=NULL; // request default subroutine
- error=VBLInstall(&noiseVBL,device,frames);
- if(error)PrintfExit("VBLInstall error %d\n",error);
- noiseVBL.vbl.vblCount=1; // enable the interrupt service routine
- while(noiseVBL.framesLeft || noiseVBL.newFrame){
- if(noiseVBL.newFrame){
- noiseVBL.newFrame=0;
- CopyBitsQuickly((BitMap *)&(movie[noiseVBL.frame])
- ,(BitMap *)*((CGrafPtr)window)->portPixMap
- ,&noiseImage[0].bounds,&window->portRect,srcCopy,NULL);
- }
- }
- VBLRemove(&noiseVBL);
-
- Note the while(noiseVBL.framesLeft || noiseVBL.newFrame). When we get to the
- last frame framesLeft will become zero and newFrame will become 1. We then
- zero newFrame, and show our last frame. framesLeft and newFrame will remain zero.
- (Thanks to Stefan Treue, treue@uni-tuebingen.de, for this correction.)
-
- Apple warns that interrupt service routines and any data that they access must
- be locked into memory, not swapped out, e.g. due to operation of the Memory
- Manager or Virtual Memory. It is dangerous to allow your compiler to install
- debugging code into interrupt service routines. It is VERY important that you
- call VBLRemove() before quitting. Once installed, slot-based interrupt tasks
- keep going forever. (Turning off vblData->vbl.vblCount disables the routine, but
- doesn't remove it.) The tasks are not removed by the Finder or System when your
- application finishes, even though the interrupt service routine's code and data
- will probably be overwritten.
-
- Actually, things aren't that bad, because I've added a call to _atexit()
- requesting that all the VBL tasks installed by VBLInstall() be removed whenever
- the program terminates, whether normally or abnormally. This prevents the
- otherwise very annoying crash that would accompany premature termination, e.g.
- by typing command-., while the VBL task is active.
-
- Writing an interrupt service routine, to be called once per video frame, is
- slightly tricky. VBLInterruptServiceRoutine does all the dirty work, and then
- calls a user-supplied subroutine that does whatever you want. If all you want to
- do is advance a frame counter until you reach the desired number of frames then
- you may wish to use the default FrameSubroutine() instead of writing your own.
- Put the address of whatever routine you want to use into the "subroutine"
- element of the VBLTaskAndA5 structure, or, to request use of FrameSubroutine,
- supply the address NULL. However, if your compiler uses Universal Headers, then you
- must supply a Universal Procedure Pointer instead of the subroutine address:
-
- #if GENERATINGCFM
- noiseVBL.subroutine=NewVBLProc(myRoutine);
- #else
- noiseVBL.subroutine=myRoutine;
- #endif
-
- FrameSubroutine() uses Microseconds(). If this program runs on an old System
- (pre 7) lacking the Microseconds trap, then
- SimpleVBLSubroutine() will be used instead. Microseconds() is used to discard
- spurious interrupts that occur within 3 ms of the previous. This is necessary
- because some 1991-1992 Apple video cards generate several interrupts during the
- vertical blanking interval, even though Apple's documentation clearly indicates
- that they should only generate one. The fix (holding off for 3 ms) was suggested
- by Raynald Comtois.
-
- The 1992 Apple "Inside Macintosh: Processes" book explains how to code a task
- that is to be performed each time your video device produces a vertical blanking
- level (i.e. between video frames). It's quite tricky. Therefore I wrote a
- generic one, that in turn, calls your custom one, after all the tricky bits have
- been taken care of.
-
- There are some subtleties to the VBL interrupt service routine. The main one is
- that all the Macintosh compilers reference global variables relative to register
- A5, but register A5 may have the wrong value (corresponding to a different
- application) at interrupt time. So we save the right value of A5 inside our
- structure and restore A5 before calling our Task().
-
- A further subtlety is that the new generation of optimizing compilers might
- notice that A5 is being modified, so it would be dangerous to access globals at
- all in InterruptServiceRoutine(), even though it's safe to do so in Task(). In
- fact Task() doesn't use any globals, but it could.
-
- The fact that the address of our structure is in register A0 when the interrupt
- service routine is called results from the fact that the low level hardware VBL
- interrupt is intercepted by the operating system, which then calls our routine.
-
- The VBLTaskAndA5 struct extends Apple's VBLTask struct by adding some useful
- information at the end. You may choose to copy this, or define your own, adding
- more stuff at the end. However, you may not want to bother, considering that
- your interrupt service routine can freely access global variables.
- Alternatively, I added a generic pointer at the end, which you may use to pass
- the address of any stuff that you want to access within your subroutine.
-
- Macintosh Technical Note 180 ("Multifinder Miscellanea"), p. 5 says:
- "GetVBLRec() returns the address of the VBLRec associated with our VBL task.
- This works because on entry into the VBL task, A0 points to the theVBLTask field
- in the VBLRec record, which is the first field in the record and that is the
- address we return. Note that this method works whether the VBLRec is allocated
- globally, in the heap...or...on the stack. ... This trick allows us to get to
- the saved A5, but it could also be used to get to anything we wanted to store in
- the record." (quoted by Jamie McCarthy in comp.sys.mac.prog 10/15/92.)
-
- HISTORY:
- 8/22/92 dgp wrote it, based on code extracted from my NoiseVBL.c
- 8/26/92 dgp added VBLRemoveAll(), which is automatically placed in the _atexit()
- queue, so it all your VBL tasks will be removed from the queue when
- your program exits, whether normal or abnormally.
- 9/9/92 dgp fixed erroneous attempt to use slot zero when NULL device was passed.
- 9/10/92 dgp added calls to VM to HoldMemory() and UnHoldMemory(). According to Apple's
- Memory book this isn't strictly necessary, since VBL tasks will
- be called only when it's safe.
- 9/17/92 dgp Transferred FrameSubroutine() here from GDFrameRate.c. Now use
- FrameSubroutine() instead of SimpleVBLSubroutine() as the default,
- provided we can use Timer.c, otherwise fall back to using
- SimpleVBLSubroutine.
- 10/9/92 dgp Automatically set up and dispose of the timer used by FrameSubroutine.
- Added a field to the VBLTaskAndA5 structure to hold the timer pointer,
- instead of using up the generic ptr.
- WARNING: old programs (written during 9/92) that explicitly request use of
- FrameSubroutine must be changed, because at that time it was the user's
- responsibility to set up and dispose of the timer, whereas it's now done
- for you. To remind you to make this change "FrameSubroutine" is no longer
- in VideoToolbox.h, it's treated as a private routine here. To obtain the
- services of FrameSubroutine, just specify a NULL in the VBLTaskAndA5
- subroutine field.
- 11/17/92 dgp In VBLRemove(), first disable the interrupt, then clean up.
- Fixed error that could cause bus error in VBLRemoveAll.
- VBLInstall() now installs VBLRemoveAll() only once in the _atexit()
- queue, no matter how many times you call it.
- 11/24/92 dgp Minor updating of comments.
- 7/9/93 dgp Test MATLAB in if() instead of #if.
- 2/28/94 dgp In response to query by Mike Tarr (tarr-michael@CS.YALE.EDU), changed
- frames argument from int to long, and now allow frames==-1 to request
- that the interrupt service routine continue working indefinitely.
- 3/5/94 dgp In response to a bug report by Mike Tarr, finished the 2/28/94 change,
- which I'd foolishly only done to SimpleVBLSubroutine and not to
- FrameSubroutine.
- 5/28/94 dgp Made compatible with Apple's Universal Headers. I also attempted to
- make the code PowerPC compatible, but that remains to be tested.
- 10/1/94 dgp Added new "frame" field to VBLTaskAndA5 struct, which counts up from zero.
- 10/24/94 dgp Made declaration of GetA0() explicitly indicate that answer is returned in D0, for
- better compatibility with Metrowerks CodeWarrior C.
- 10/27/94 dgp It is very annoying for the machine to crash anytime you try to quit your application
- by escaping via MacsBugs escape to shell. The problem is that CodeWarrior 4.5 doesn't attach
- the atexit() tasks to _EscapeToShell. So I do it here.
- 5/3/95 dgp fixed test for presence of vm, which before was always returning false.
- 7/1/95 dgp just call AtExitToShell(), without any conditionals. Special cases, e.g. MATLAB,
- are handled in AtExitToShell.c.
- 9/5/96 st Stefan Treue corrected an error in the frame counting in the example above.
- Added Stefan's enhanced FrameSubroutineElapsed at end of file.
- 3/19/97 dgp Eliminated the obsolete disabled code that used Timer.c.
- Added "seconds" field to the VBLTaskAndA5 struct to provide the same information
- (time of last VBL) as the existing microTicks field, but in a more convenient form.
- 3/20/97 dgp David Brainard reported that this was crashing on 68K. Turns out that interrupt
- service routines aren't allowed to use the 68881 fpu unless they save and restore
- the fpu registers. And Apple advises against using it at all since some of its
- operations are very lengthy and you have to wait for the current operation to
- end. So we now call Seconds() only if GENERATINGPOWERPC.
- 3/26/97 dgp Call Seconds() only if !GENERATING68881.
- 3/26/97 dgp HoldMemory on Seconds() if vmPresent and USE_SECONDS.
- 3/26/97 dgp Call Seconds() only if !GENERATING68K, since i'm not sure the call will work if
- we don't set up the A4 (not A5) register for access to globals.
- 4/10/97 dgp Eliminate stuff that was conditional on !UNIVERSAL_HEADERS.
- 4/14/97 dgp Add A4 stuff to support accessing globals when compiled as 68K code resource.
- 4/14/97 dgp Add call to HighPriorityTimeout.
- 4/21/97 dgp Standardized the definition of SetA4().
- 4/21/97 dgp Standardized the definition of GetA0().
- 4/21/97 dgp Standardized the definition of GetA4() & GetA5().
- 5/30/97 dgp Removed call to HighPriorityTimeout.
- 7/31/97 dgp Made VBLTaskAndA5.microTicks and VBLTaskAndA5.seconds volatile, since they're modified
- at interrupt time. This change is in VideoToolbox.h.
- 8/10/97 dgp Added (UnsignedWide *) cast in call to Microseconds to make THINK C happy.
- */
- #include "VideoToolbox.h"
- #ifndef __TRAPS__
- #include <Traps.h>
- #endif
- //#include <Retrace.h>
- void VBLRemoveAll(void);
- #if GENERATINGPOWERPC
- void VBLInterruptServiceRoutine(register VBLTaskAndA5 *vblData);
- #else
- void VBLInterruptServiceRoutine(void);
- #endif
- static void FrameSubroutine(VBLTaskAndA5 *vblData);
-
- // This macro allows you to disable the use of Seconds() if that routine is too
- // slow on your computer to be called from an interrupt service routine.
- #if GENERATING68K // Defined in Apple's ConditionalMacros.h
- #define USE_SECONDS 0 // This MUST be 0, or 68881 will crash.
- #else
- #define USE_SECONDS 1 // Put time of VBL into "seconds" field of VBLTaskAndA5 struct.
- #endif
-
- #if GENERATING68K
- #pragma parameter __D0 GetA4()
- long GetA4(void)= 0x200C; // MOVE.L A4,D0
- #pragma parameter __D0 GetA5()
- long GetA5(void)= 0x200D; // MOVE.L A5,D0
- #if THINK_C
- #pragma parameter __D0 SetA4(__D0)
- long SetA4(long) = 0xC18C; // EXG D0,A4
- #else
- long SetA4(long:__D0):__D0 = 0xC18C; // EXG D0,A4
- #endif
- #else
- #define GetA4() 0L
- #define GetA5() 0L
- #define SetA4(x) 0L
- #endif
-
- /*
- I don't know how to tell whether we're being compiled
- as an application or code resource.
- In a code resource, A4 is used as the global pointer.
- In an application, it's A5. So we have to know.
- At present we assume that we're in an application
- unless we're being compiled as a code resource for MATLAB.
- */
- #define CODE_RESOURCE MATLAB
-
- /* This is just for reference. The original is in VideoToolbox.h
- struct VBLTaskAndA5 {
- volatile VBLTask vbl;
- long ourA5;
- #if USESROUTINEDESCRIPTORS || GENERATINGCFM
- UniversalProcPtr subroutine;
- #else
- void (*subroutine)(struct VBLTaskAndA5 *vblData);
- #endif
- GDHandle device;
- long slot;
- volatile long newFrame; // Boolean
- volatile long frame; // count up from zero
- volatile long framesLeft; // count down to zero
- long framesDesired;
- volatile UnsignedWide microTicks; // Microseconds() at time of most recent VBL
- volatile double seconds; // Seconds() at time of most recent VBL
- void *ptr; // use this for whatever you want
- };
- typedef struct VBLTaskAndA5 VBLTaskAndA5;
- */
- #define TASK_SIZE 1000 // Generous guess for size of routine
- static long vmPresent=0;
- static VBLUPP vblProcPtr,frameSubroutineProcPtr,simpleVBLSubroutineProcPtr;
-
- OSErr VBLInstall(VBLTaskAndA5 *vblData,GDHandle device,long frames)
- // trivial, but verbose
- {
- static Boolean firstTime=1,slotRoutinesAvailable,microsecondsAvailable;
- static long timeManagerVersion=0;
-
- if(firstTime){
- firstTime=0;
- slotRoutinesAvailable=TrapAvailable(_SlotVInstall);
- Gestalt(gestaltVMAttr,&vmPresent);
- vmPresent &= 1L<<gestaltVMPresent;
- AtExitToShell(VBLRemoveAll);
- Gestalt(gestaltTimeMgrVersion,&timeManagerVersion);
- vblProcPtr=NewVBLProc(VBLInterruptServiceRoutine);
- frameSubroutineProcPtr=NewVBLProc(FrameSubroutine);
- simpleVBLSubroutineProcPtr=NewVBLProc(SimpleVBLSubroutine);
- // This is Apple's recommended way to determine whether the Microseconds
- // trap is available. <http://devworld.apple.com/dev/qa/tb/tb16.html>
- #if !defined(_Microseconds)
- #define _Microseconds 0xa193 // from traps.h
- #endif
- microsecondsAvailable=TrapAvailable(_Microseconds);
- #if USE_SECONDS
- Seconds(); // call it once now, so we don't do initialization at VBL time.
- #endif
- }
- vblData->device=device;
- if(device!=NULL && (*device)->gdRefNum!=0 && slotRoutinesAvailable)
- vblData->slot=GetDeviceSlot(device);
- else vblData->slot=-1;
- vblData->vbl.vblAddr=(void *)vblProcPtr;
- vblData->vbl.qType=vType;
- vblData->vbl.vblCount=0; /* Initially disable the interrupt service routine */
- vblData->vbl.vblPhase=0;
- #if CODE_RESOURCE
- vblData->ourA5=GetA4();
- #else
- vblData->ourA5=GetA5();
- #endif
- if(vblData->subroutine==NULL){
- vblData->seconds=Nan;
- vblData->microTicks.lo=vblData->microTicks.hi=0;
- if(microsecondsAvailable){
- Microseconds((UnsignedWide *)&vblData->microTicks); // cast to satisfy THINK C
- vblData->subroutine=(void *)frameSubroutineProcPtr;
- }else vblData->subroutine=(void *)simpleVBLSubroutineProcPtr;
- }
- vblData->newFrame=0;
- vblData->frame=0;
- vblData->framesLeft=vblData->framesDesired=frames;
- if(vmPresent){
- HoldMemory(vblData,sizeof(*vblData));
- HoldMemory(vblData->vbl.vblAddr,TASK_SIZE);
- HoldMemory(vblData->subroutine,TASK_SIZE);
- #if USE_SECONDS
- HoldMemory(Seconds,TASK_SIZE);
- #endif
- }
- if(vblData->slot>=0)return SlotVInstall((QElemPtr)vblData,vblData->slot);
- else return VInstall((QElemPtr)vblData);
- }
-
- OSErr VBLRemove(VBLTaskAndA5 *vblData)
- {
- int error;
-
- // only remove it if we installed it
- if(vblData->vbl.vblAddr != vblProcPtr)return 0;
- if(vblData->slot>=0)error=SlotVRemove((QElemPtr)vblData,vblData->slot);
- else error=VRemove((QElemPtr)vblData);
- if(vmPresent){
- UnholdMemory(vblData,sizeof(vblData));
- UnholdMemory(vblData->vbl.vblAddr,TASK_SIZE);
- UnholdMemory(vblData->subroutine,TASK_SIZE);
- #if USE_SECONDS
- UnholdMemory(Seconds,TASK_SIZE);
- #endif
- }
- return error;
- }
-
- void VBLRemoveAll(void)
- {
- QHdrPtr qHeader;
- QElemPtr q;
-
- qHeader=GetVBLQHdr();
- q=qHeader->qHead;
- while(q!=NULL){
- VBLRemove((VBLTaskAndA5 *)q); // only removes ours
- q=q->qLink;
- }
- }
-
- #if (THINK_C || THINK_CPLUS || SYMANTEC_C)
- #pragma options(!profile) // it would be dangerous to call the profiler from here
- #pragma options(assign_registers,redundant_loads)
- #endif
- #if __MWERKS__ && __profile__
- #pragma profile off // on 68k it would be dangerous to call the profiler from here
- #endif
-
- #if GENERATINGPOWERPC
- void VBLInterruptServiceRoutine(register VBLTaskAndA5 *vblData)
- {
- CallVBLProc(vblData->subroutine,vblData); // call user's task, pass data ptr
- }
- #else
- #if THINK_C
- #pragma parameter __D0 GetA0
- long GetA0(void)=0x2008; /* MOVE.L A0,D0 */
- #else
- Ptr GetA0(void):__D0 =0x2008; /* MOVE.L A0,D0 */
- #endif
-
- void VBLInterruptServiceRoutine(void)
- {
- register long oldA;
- register VBLTaskAndA5 *vblData;
-
- vblData = (VBLTaskAndA5 *)GetA0();
- #if CODE_RESOURCE
- oldA = SetA4(vblData->ourA5);
- #else
- oldA = SetA5(vblData->ourA5);
- #endif
- // call user's task, pass data ptr
- #if GENERATINGCFM
- // WARNING: uppVBLProcInfo is the wrong selector.
- CallUniversalProc(vblData->subroutine,uppVBLProcInfo,vblData);
- #else
- (*vblData->subroutine)(vblData);
- #endif
- #if CODE_RESOURCE
- SetA4(oldA);
- #else
- SetA5(oldA);
- #endif
- }
- #endif
-
- // WARNING: setting/restoring A4 in VBLInterruptServiceRoutine, above,
- // hasn't yet been tested. It's only relevant if we access globals
- // and compile as a 68K code resource.
- void SimpleVBLSubroutine(VBLTaskAndA5 *vblData)
- {
- vblData->newFrame=1;
- vblData->frame++;
- if(vblData->framesLeft>0)vblData->framesLeft--;
- if(vblData->framesLeft!=0)vblData->vbl.vblCount=1;
- }
-
-
- // WARNING: setting/restoring A4 in VBLInterruptServiceRoutine, above,
- // hasn't yet been tested. It's only relevant if we access globals
- // and compile as a 68K code resource.
- static void FrameSubroutine(VBLTaskAndA5 *vblData)
- {
- // The 1991-2 Apple video cards emit several vbl interrupts per frame,
- // which violates Apple's documentation.
- // So we ignore any interrupt that occurs within 3 ms
- // of the most recent interrupt for that video device.
- // Thus this frame counter should work correctly on all video cards.
- // Suggested by Raynald Comtois.
-
- UnsignedWide microTicks;
- unsigned long microS;
-
- Microseconds(µTicks);
- #if USE_SECONDS
- vblData->seconds=Seconds();
- #endif
- microS=microTicks.hi-vblData->microTicks.hi;
- if(microS==0)
- // the usual case
- microS=microTicks.lo-vblData->microTicks.lo;
- else
- // can happen at most once every 36 minutes
- microS=0xffffffffUL-vblData->microTicks.lo+microTicks.lo+1;
- if(microS>3000){
- // It's been more than 3 ms since last interrupt, so this one is valid.
- vblData->microTicks=microTicks;
- vblData->newFrame=1; // set new-frame flag
- vblData->frame++;
- if(vblData->framesLeft>0)vblData->framesLeft--;
- if(vblData->framesLeft!=0)vblData->vbl.vblCount=1; // re-enable interrupt
- }else vblData->vbl.vblCount=1; // re-enable interrupt
- }
-